package cn.maiyar.log;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.maiyar.log.ttl.ApplicationContextProvider;
import cn.maiyar.dingding.feign.IDingDingClient;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import java.util.*;

import static java.util.stream.Collectors.toList;

/**
 * 异步发送消息
 *
 * @author c.c
 */
@Slf4j
@Setter
@Getter
@ToString
public class LogbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

    // 服务名称
    private String serviceName;
    // IP:PORT
    private String server;
    // 当前环境
    private String active;
    // traceId
    private String traceId;

    Layout<ILoggingEvent> layout;

    @Override
    public void start() {
        if (this.layout == null) {
            log.debug("LogDingDingAppender Layout未被初始化!");
        }
        log.debug("LogDingDingAppender 自定义Appender已完成初始化");
        super.start();
    }

    @Override
    public void stop() {
        // 释放相关资源，如数据库连接，redis线程池等等
        log.debug("LogDingDingAppender logback-stop方法被调用");
        if (!isStarted()) {
            return;
        }
        super.stop();
    }

    @Override
    protected void append(ILoggingEvent event) {
        if (event == null || !isStarted()) {
            log.debug("LogDingDingAppender 日志事件为空或该Appender未被初始化");
            return;
        }
        Level level = event.getLevel();
        if (!level.isGreaterOrEqual(Level.ERROR)) {
            return;
        }
        // 通知相关业务配置
        NoticeProperties noticeProperties = ApplicationContextProvider.getBean(NoticeProperties.class);
        // 是否开启通知
        boolean enabled = noticeProperties.isEnabled();
        if (!enabled) {
            return;
        }

        StringBuilder sb = new StringBuilder();
        sb.append("【应用程序】").append(serviceName).append(":").append(server).append("\n");
        sb.append("【运行环境】").append(active).append("\n");
        String traceId = MDC.get("traceId");
        if (StrUtil.isNotEmpty(traceId)) {
            sb.append("【链路ID】").append(traceId).append("\n");
        }
        IThrowableProxy iThrowableProxy = event.getThrowableProxy();
        if (iThrowableProxy instanceof ThrowableProxy) {
            ThrowableProxy throwableProxy = (ThrowableProxy) iThrowableProxy;
            Throwable throwable = throwableProxy.getThrowable();
            // 不需要通知的异常类
            List<Class<? extends Exception>> excludeExceptions = noticeProperties.getExcludeExceptions();
            List<Class<? extends Throwable>> includeExceptions = this.getAllExceptionClazz(throwable);
            for (Class<? extends Exception> excludeException : excludeExceptions) {
                if (includeExceptions.stream().anyMatch(excludeException::isAssignableFrom)) {
                    return;
                }
            }
            // 异常信息
            List<String> exceptionMessage = this.exceptionMessage(throwable);
            // 异常追踪信息
            List<String> stackTraceMessages = new ArrayList<>();
            // 方法名
            String methodName = null;
            // 类路径
            String className = null;

            // 追踪信息的包含的包名 默认为 cn.maiyar
            String includedTracePackage = noticeProperties.getIncludedTracePackage();
            List<StackTraceElement> stackTraceElements = this.stackTrace(throwable, includedTracePackage);
            if (CollUtil.isNotEmpty(stackTraceElements)) {
                // 异常追踪信息
                stackTraceMessages = stackTraceElements.stream().map(StackTraceElement::toString).collect(toList());
                // 方法名
                methodName = stackTraceElements.get(0).getMethodName();
                // 类路径
                className = stackTraceElements.get(0).getClassName();
            }
            if (StrUtil.isNotEmpty(className)) {
                sb.append("【类路径】").append(className).append("\n");
            }
            if (StrUtil.isNotEmpty(methodName)) {
                sb.append("【方法名】").append(methodName).append("\n");
            }
            if (CollUtil.isNotEmpty(exceptionMessage)) {
                sb.append("【异常信息】").append(exceptionMessage).append("\n");
            }
            if (CollUtil.isNotEmpty(stackTraceElements)) {
                sb.append("【异常追踪】").append(stackTraceMessages).append("\n");
            }
        } else {
            return;
        }
        sb.append("【错误时间】").append(new Date()).append("\n");

        IDingDingClient dingClient = ApplicationContextProvider.getBean(IDingDingClient.class);
        dingClient.doSend(sb.toString());
    }

    /**
     * 获取所有异常的类信息
     *
     * @param throwable
     * @return
     */
    private List<Class<? extends Throwable>> getAllExceptionClazz(Throwable throwable) {
        List<Class<? extends Throwable>> includeExceptions = new LinkedList<>();
        includeExceptions.add(throwable.getClass());
        Throwable cause = throwable.getCause();
        while (cause != null) {
            includeExceptions.add(cause.getClass());
            cause = cause.getCause();
        }
        return includeExceptions;
    }

    /**
     * 获取异常信息
     *
     * @param throwable
     * @return
     */
    private List<String> exceptionMessage(Throwable throwable) {
        List<String> messages = new LinkedList<>();
        messages.add(String.format("%s:%s", throwable.getClass().getName(), throwable.getMessage()));
        Throwable cause = throwable.getCause();
        if (cause != null) {
            exceptionMessage(cause);
        }
        return messages;
    }

    private List<StackTraceElement> stackTrace(Throwable throwable, String filterTrace) {
        List<StackTraceElement> list = new LinkedList<StackTraceElement>();
        addStackTrace(list, throwable, filterTrace);
        Throwable cause = throwable.getCause();
        while (cause != null) {
            addStackTrace(list, cause, filterTrace);
            cause = cause.getCause();
        }
        return list;
    }

    public void addStackTrace(List<StackTraceElement> list, Throwable throwable, String filterTrace) {
        list.addAll(0,
                Arrays.stream(throwable.getStackTrace())
                        .filter(x -> filterTrace == null || x.getClassName().startsWith(filterTrace))
                        .filter(x -> !Objects.equals(x.getFileName(), "<generated>")).collect(toList()));
    }
}
